/*
 * Copyright 2009 (c) Patrick Pietens.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package indigo.model
{
	import indigo.errors.IndigoError;
	import indigo.events.PropertyChangeEvent;
	import indigo.utils.DisposableEventDispatcher;
	import indigo.utils.IDisposable;

	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.utils.Dictionary;
	import flash.utils.Proxy;
	import flash.utils.flash_proxy;
	import flash.utils.getQualifiedClassName;

	use namespace flash_proxy;
	/**
	 * Dispatches when a property is added to a <code>BindableObject</code>
	 * @eventType indigo.events.PropertyChangeEvent
	 */
	[Event(name="propertyAdded", type="indigo.events.PropertyChangeEvent")]
	/**
	 * Dispatches when a property is added to a <code>BindableObject</code>
	 * @eventType indigo.events.PropertyChangeEvent
	 */
	[Event(name="propertyChanged", type="indigo.events.PropertyChangeEvent")]
	/**
	 * Dispatches when a property is added to a <code>BindableObject</code>
	 * @eventType indigo.events.PropertyChangeEvent
	 */
	[Event(name="propertyUnchanged", type="indigo.events.PropertyChangeEvent")]
	/**
	 * Dispatches when a property is added to a <code>BindableObject</code>
	 * @eventType indigo.events.PropertyChangeEvent
	 */
	[Event(name="propertyDeleted", type="indigo.events.PropertyChangeEvent")]
	/**
	 * Like <code>ValueObjects</code>, a <code>BindableObject</code> hold no 
	 * functionality but data instead. The difference between the both is that 
	 * <code>BindableObjects</code> will dispatch a <code>PropertyChangeEvent</code>
	 * when its data is updated, deleted or when a new property is added. 
	 * 
	 * <p><b>Note:</b> The only drawback to this is that
	 * <code>BindableObjects</code> are loosely typed. A workaround is to create
	 * a <code>State</code> class that holds all property names.<p> 
	 * 
	 * <p>You can listen to <code>PropertyChangeEvents</code> of all properties 
	 * or to a specific property. Use the  <code>watchProperty</code> and 
	 * <code>unwatchProperty</code> methods.</p>   
	 * 
	 * @example
	 * <listing version="3.0">
	 * </listing>
	 * 
	 * @playerversion Flash 9
	 * @langversion ActionScript 3.0
	 * @see	ModelLocator
	 * 
	 */
	public dynamic class BindableObject extends Proxy implements IDisposable
	{
		/**
		 * EventDispatcher that is used to dispatch the BindableObject's events
		 */
		protected var _eventDispatcher:DisposableEventDispatcher;
		/**
		 * Dictionary that holds all eventListeners
		 */
		protected var _eventList:Dictionary = new Dictionary( true );
		/**
		 * Dictionary that holds all properties of the BindableObject
		 */
		protected var _propertyList:Dictionary = new Dictionary( true );
		protected var _bindList:Array = new Array();


		/**
		 * Constructor
		 * 
		 */
		public function BindableObject()
		{
			super();
			this.initialize();
		}


		/**
		 * Executes when the BindableObject is instantiated
		 * 
		 */
		private function initialize():void
		{
			this._eventDispatcher = new DisposableEventDispatcher;
		}


		/**
		 * @inheritDoc 
		 * 
		 */
		override flash_proxy function setProperty( name:*, value:* ):void
		{
			var myEvent:PropertyChangeEvent;
			var myOldValue:* = this._propertyList[ name ];

			// Adds or updates the propertyList
			this._propertyList[ name ] = value;

			// If the property already exists
			if ( myOldValue )
			{
				// Send a PropertyChanged event when the property is changed
				if ( myOldValue != value )
				{
					log( "Updated property '" + name + "': '" + value + "'" );
					myEvent = new PropertyChangeEvent( PropertyChangeEvent.CHANGED );
				}
					
				// Send PropertyUnchanged event otherwise
				else
					myEvent = new PropertyChangeEvent( PropertyChangeEvent.UNCHANGED );
			}
			
			// Send a PropertyAdded event when the property didn't exists before
			else
			{
				log( "Added property '" + name + "': '" + value + "'" );
				myEvent = new PropertyChangeEvent( PropertyChangeEvent.ADDED );
			}

			// Define the PropertyChangeEvent
			myEvent.property = name;
			myEvent.oldValue = myOldValue;
			myEvent.newValue = value;

			this.dispatchEvent( myEvent );
		}


		/**
		 * @inheritDoc 
		 * 
		 */
		override flash_proxy function getProperty( name:* ):*
		{
			// Throws an error when the property isn't found
			if ( !this.flash_proxy::hasProperty( name ) )
				throw new Error( IndigoError.PROPERTY_NOT_FOUND + ": " + name );

			return this._propertyList[ name ];
		}


		/**
		 * @inheritDoc 
		 * 
		 */
		override flash_proxy function deleteProperty( name:* ):Boolean
		{
			// Throws an error when the property isn't found
			// if ( !this.flash_proxy::hasProperty( name ) )
			// throw new Error( IndigoError.PROPERTY_NOT_FOUND + ": " + name );

			// Dispatch event
			var myEvent:PropertyChangeEvent = new PropertyChangeEvent( PropertyChangeEvent.DELETED );
			myEvent.property = name;
			myEvent.oldValue = this._propertyList[ name ];

			this.dispatchEvent( myEvent );

			// Delete property
			delete this._propertyList[ name ];
			this._propertyList[ name ] = null;

			log( "Deleted property '" + name + "'" );
			return true;
		}


		public function bind( name:String, host:Object, chain:String ):void
		{
			var myActor:BindActor = new BindActor( name, host, chain );
			this._bindList.push( myActor );
		}


		public function unbind( name:String, host:*, chain:* ):void
		{
			var myActor:BindActor;

			var i:int = this._bindList.length;
			while ( --i > -1 )
			{
				myActor = this._bindList[ i ] as BindActor;
				if ( myActor.property == name && myActor.host == host && myActor.chain == chain )
				{
					myActor.dispose();
					this._bindList.splice( i, 1 );
				}
			}
		}


		/**
		 * Returns a Boolean that indicates that the BindableObject has a property 
		 * 
		 */
		override flash_proxy function hasProperty( name:* ):Boolean
		{
			return this._propertyList[ name ] != null;
		}


		/**
		 * @inheritDoc
		 * 
		 */
		override flash_proxy function callProperty( name:*, ...args ):*
		{
			return Function( this[ name ] ).apply( this, args );
		}


		/**
		 * Marks a property as dirty. When set to dirty a 
		 * PropertyChangeEvent.CHANGED is dispatched. Use this to notify listeners
		 * that primitives having own properties are updated.
		 * @param	String that indicates the name of the property
		 *  
		 */
		public function setDirty( name:String ):void
		{
			if ( !this.hasProperty( name ) )
				throw new Error( IndigoError.PROPERTY_NOT_FOUND + ": " + name );

			var myEvent:PropertyChangeEvent = new PropertyChangeEvent( PropertyChangeEvent.CHANGED );
			myEvent.property = name;
			myEvent.oldValue = null;
			myEvent.newValue = this._propertyList[ name ];

			this.dispatchEvent( myEvent );
		}


		/**
		 * @inheritDoc
		 *
		 */
		public function addEventListener( type:String, listener:Function, property:String = null, useCapture:Boolean = false, priority:int = 0, useWeakReference:Boolean = false ):void
		{
			// If the listener is for a specific property
			if ( property )
			{
				// Check if the property has it's own listener
				if ( !this.hasSpecificEventListener( property ) )
					this._eventList[ property ] = new DisposableEventDispatcher();

				// Add listener
				DisposableEventDispatcher( this._eventList[ property ] ).addEventListener( type, listener, useCapture, priority, useWeakReference );
			}
			else
				this._eventDispatcher.addEventListener( type, listener, useCapture, priority, useWeakReference );
		}


		/**
		 * @inheritDoc
		 *
		 */
		public function removeEventListener( type:String, listener:Function, property:String = null, useCapture:Boolean = false ):void
		{
			this._eventDispatcher.removeEventListener( type, listener, useCapture );
			if ( this.hasSpecificEventListener( property ) )
				DisposableEventDispatcher( this._eventList[ property ] ).removeEventListener( type, listener, useCapture );
		}


		/**
		 * @inheritDoc
		 * 
		 */
		public function dispatchEvent( event:PropertyChangeEvent ):Boolean
		{
			// Dispatch the event
			if ( this.hasEventListener( event.type ) )
				this._eventDispatcher.dispatchEvent( event );

			// Dispatch the event to all listeners of this specific property
			if ( this.hasSpecificEventListener( event.property ) )
				( this._eventList[ event.property ] as EventDispatcher ).dispatchEvent( event.clone() );

			return true;
		}


		/**
		 * @inheritDoc
		 *
		 */
		public function hasEventListener( type:String ):Boolean
		{
			return this._eventDispatcher.hasEventListener( type );
		}


		/**
		 * Returns whether an event listener to a specific property exists.
		 * @param	String that indicates the Event type
		 * @param	String that indicates the property name
		 * @return	Boolean that indicates the listener exists
		 * 
		 */
		public function hasSpecificEventListener( property:String ):Boolean
		{
			return this._eventList[ property ] != null;
		}


		/**
		 * @inheritDoc
		 *
		 */
		public function willTrigger( type:String ):Boolean
		{
			return this._eventDispatcher.willTrigger( type );
		}


		/**
		 * @inheritDoc
		 * 
		 */
		public function toString():String
		{
			var myClassName:String = getQualifiedClassName( this ).split( "::" )[ 1 ];
			var myString:String = "[" + myClassName + " ";

			for ( var property:String in this._propertyList )
				myString += property + "=" + this._propertyList[ property ] + " ";
			myString += "]";

			return myString;
		}


		/**
		 * @inheritDoc
		 * 
		 */
		public function dispose( event:Event = null ):void
		{
			log( "Disposing BindableObject" );

			this._eventDispatcher.dispose();

			var myActor:BindActor;

			var i:int = this._bindList.length;
			while ( --i > -1 )
			{
				myActor = this._bindList[ i ] as BindActor;
				myActor.dispose();
			}

			// Remove all properties
			for ( var property:String in this._propertyList )
			{
				this._propertyList[ property ] = null;
				delete this._propertyList[ property ];
			}

			// Nullify objects
			this._bindList = null;
			this._eventList = null;
			this._eventDispatcher = null;
			this._propertyList = null;
		}
	}
}
import indigo.events.PropertyChangeEvent;
import indigo.model.ApplicationModel;
import indigo.utils.IDisposable;

import flash.events.Event;
import flash.utils.flash_proxy;

internal class BindActor implements IDisposable
{
	private var _property:String;
	private var _host:Object;
	private var _chain:String;


	public function BindActor( property:String, host:Object, chain:String ):void
	{
		this._property = property;
		this._host = host;
		this._chain = chain;

		if ( ApplicationModel.getInstance().flash_proxy::hasProperty( property ) )
			this._host[ this._chain ] = getProperty( property );

		watchProperty( property, this.onPropertyChange );
	}


	private function onPropertyChange( event:PropertyChangeEvent ):void
	{
		this._host[ this._chain ] = event.newValue;
	}


	public function get property():String
	{
		return this._property;
	}


	public function get host():*
	{
		return this._host;
	}


	public function get chain():String
	{
		return this._chain;
	}


	public function dispose( event:Event = null ):void
	{
		unwatchProperty( this._property, this.onPropertyChange );

		this._property = null;
		this._host = null;
		this._chain = null;
	}
}